﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace Aeon.Emulator.Dos.VirtualFileSystem
{
    /// <summary>
    /// Serves as the file system for DOS.
    /// </summary>
    public sealed class FileSystem
    {
        #region Private Fields
        /// <summary>
        /// List of emulated drives.
        /// </summary>
        private readonly DriveList drives = new DriveList();
        /// <summary>
        /// List of current directories.
        /// </summary>
        private readonly VirtualPath[] currentDirectories = new VirtualPath[26];
        /// <summary>
        /// Current drive.
        /// </summary>
        private DriveLetter currentDrive = DriveLetter.C;
        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the FileSystem class.
        /// </summary>
        public FileSystem()
        {
            for(int i = 0; i < currentDirectories.Length; i++)
                currentDirectories[i] = VirtualPath.AbsoluteRoot;
            
            this.Drives[DriveLetter.C].DriveType = DriveType.Fixed;
        }
        #endregion

        #region Public Events
        /// <summary>
        /// Raised when the CurrentDrive property has changed.
        /// </summary>
        public event EventHandler CurrentDriveChanged;
        #endregion

        #region Public Properties
        /// <summary>
        /// Gets or sets the current drive and directory in the DOS session.
        /// </summary>
        public VirtualPath WorkingDirectory
        {
            get
            {
                return ResolvePath(VirtualPath.RelativeCurrent);
            }
            set
            {
                if(value == null)
                    throw new ArgumentNullException("value");
                if(value.PathType != VirtualPathType.Absolute)
                    throw new ArgumentException("Path must be absolute.");
                if(value.DriveLetter == null)
                    throw new ArgumentException("Path must specify drive.");

                this.currentDrive = (DriveLetter)value.DriveLetter;
                this.currentDirectories[this.currentDrive.Index] = value.ChangeDrive(null);
            }
        }
        public DriveLetter CurrentDrive
        {
            get { return this.currentDrive; }
            set
            {
                if(value != this.currentDrive)
                {
                    this.currentDrive = value;
                    this.OnCurrentDriveChanged(EventArgs.Empty);
                }
            }
        }
        /// <summary>
        /// Gets the collection of emulated drives.
        /// </summary>
        public DriveList Drives
        {
            get { return this.drives; }
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Returns an absolute path based on the current directory and drive.
        /// </summary>
        /// <param name="path">Path to make absolute.</param>
        /// <returns>Absolute path generated from the input path.</returns>
        public VirtualPath ResolvePath(VirtualPath path)
        {
            if(path == null)
                throw new ArgumentNullException("path");

            if(path.PathType == VirtualPathType.Absolute)
            {
                if(path.DriveLetter != null)
                    return path;
                else
                    return path.ChangeDrive(this.CurrentDrive);
            }
            else
            {
                if(path.DriveLetter != null)
                {
                    var newPath = this.currentDirectories[((DriveLetter)path.DriveLetter).Index] + path;
                    return newPath.ChangeDrive(path.DriveLetter);
                }
                else
                {
                    var newPath = this.currentDirectories[this.CurrentDrive.Index].ChangeDrive(this.CurrentDrive);
                    return newPath + path;
                }
            }
        }
        /// <summary>
        /// Gets the current directory for a drive in the DOS session.
        /// </summary>
        /// <param name="driveLetter">Drive letter whose directory will be returned.</param>
        /// <returns>Current directory for the specified drive.</returns>
        public VirtualPath GetCurrentDirectory(DriveLetter driveLetter)
        {
            return this.currentDirectories[driveLetter.Index] ?? VirtualPath.AbsoluteRoot;
        }
        /// <summary>
        /// Gets the current directory for a drive in the DOS session.
        /// </summary>
        /// <param name="driveLetter">Drive letter whose directory will be returned.</param>
        /// <returns>Current directory for the specified drive.</returns>
        [Obsolete]
        public VirtualPath GetCurrentDirectory(char driveLetter)
        {
            char drive = char.ToUpperInvariant(driveLetter);
            if(drive < 'A' || drive > 'Z')
                throw new ArgumentException("Drive letter must be in the range of A-Z.");

            return currentDirectories[(int)(drive - 'A')] ?? VirtualPath.AbsoluteRoot;
        }
        /// <summary>
        /// Sets the current directory for a drive in the DOS session.
        /// </summary>
        /// <param name="path">New current directory.</param>
        public void ChangeDirectory(VirtualPath path)
        {
            if(path == null)
                throw new ArgumentNullException("path");

            path = ResolvePath(path);

            var drive = path.DriveLetter ?? currentDrive;
            this.currentDirectories[drive.Index] = path.ChangeDrive(null);
        }
        /// <summary>
        /// Opens an existing file or creates a new one.
        /// </summary>
        /// <param name="path">Relative path to file in the file system.</param>
        /// <param name="fileMode">Specifies how the file should be opened.</param>
        /// <param name="fileAccess">Specifies the desired access to the file.</param>
        /// <returns>Stream backed by the requested file.</returns>
        public Stream OpenFile(VirtualPath path, FileMode fileMode, FileAccess fileAccess)
        {
            if(path == null)
                throw new ArgumentNullException("path");

            path = ResolvePath(path);

            return this.Drives[(DriveLetter)path.DriveLetter].OpenFile(path, fileMode, fileAccess);
        }
        /// <summary>
        /// Opens an existing file or creates a new one.
        /// </summary>
        /// <param name="path">Relative path to file in the file system.</param>
        /// <param name="fileMode">Specifies how the file should be opened.</param>
        /// <param name="fileAccess">Specifies the desired access to the file.</param>
        /// <returns>Stream backed by the requested file.</returns>
        public Stream OpenFile(string path, FileMode fileMode, FileAccess fileAccess)
        {
            if(path == null)
                throw new ArgumentNullException("path");

            return OpenFile(new VirtualPath(path), fileMode, fileAccess);
        }
        /// <summary>
        /// Returns a list of all of the files in a path inside a virtual directory.
        /// </summary>
        /// <param name="path">Path inside virtual directory to get list of files from.</param>
        /// <returns>List of files in the specified path.</returns>
        public IEnumerable<VirtualFileInfo> GetDirectory(VirtualPath path)
        {
            if(path == null)
                throw new ArgumentNullException("path");

            path = ResolvePath(path);

            return this.Drives[(DriveLetter)path.DriveLetter].GetDirectory(path);
        }
        /// <summary>
        /// Returns a list of all of the files in a path inside a virtual directory.
        /// </summary>
        /// <param name="path">Path inside virtual directory to get list of files from.</param>
        /// <returns>List of files in the specified path.</returns>
        public IEnumerable<VirtualFileInfo> GetDirectory(string path)
        {
            if(path == null)
                throw new ArgumentNullException("path");

            return GetDirectory(new VirtualPath(path));
        }
        /// <summary>
        /// Returns a list of all of the files in a path inside a virtual directory.
        /// </summary>
        /// <param name="path">Path inside virtual directory to get list of files from.</param>
        /// <param name="includedAttributes">File attributes to include in the search.</param>
        /// <returns>List of files in the specified path.</returns>
        public IEnumerable<VirtualFileInfo> GetDirectory(VirtualPath path, VirtualFileAttributes includedAttributes)
        {
            if(path == null)
                throw new ArgumentNullException("path");

            // This is hack. It should work as long as something isn't
            // counting on multiple volume labels on a drive.
            if((includedAttributes & VirtualFileAttributes.VolumeLabel) != 0)
            {
                path = ResolvePath(path);
                var label = this.Drives[(DriveLetter)path.DriveLetter].VolumeLabel ?? string.Empty;

                return new[] { new VirtualFileInfo(label, VirtualFileAttributes.VolumeLabel, DateTime.Now, 0) };
            }

            return this.GetDirectory(path).Where(f => (f.Attributes & FindAttributeMask & includedAttributes) == (f.Attributes & FindAttributeMask));
        }
        /// <summary>
        /// Returns a list of all of the files in a path inside a virtual directory.
        /// </summary>
        /// <param name="path">Path inside virtual directory to get list of files from.</param>
        /// <param name="includedAttributes">File attributes to include in the search.</param>
        /// <returns>List of files in the specified path.</returns>
        public IEnumerable<VirtualFileInfo> GetDirectory(string path, VirtualFileAttributes includedAttributes)
        {
            if(path == null)
                throw new ArgumentNullException("path");

            return this.GetDirectory(new VirtualPath(path), includedAttributes);
        }
        /// <summary>
        /// Gets information about a file.
        /// </summary>
        /// <param name="path">Path to file in the file system.</param>
        /// <returns>Information about the file if it is found; otherwise null.</returns>
        public VirtualFileInfo GetFileInfo(VirtualPath path)
        {
            if(path == null)
                throw new ArgumentNullException("path");

            path = ResolvePath(path);

            var info = this.Drives[(DriveLetter)path.DriveLetter].GetFileInfo(path);
            if(info != null)
                info.DeviceIndex = ((DriveLetter)path.DriveLetter).Index;

            return info;
        }
        /// <summary>
        /// Gets information about a file.
        /// </summary>
        /// <param name="path">Path to file in the file system.</param>
        /// <returns>Information about the file if it is found; otherwise null.</returns>
        public VirtualFileInfo GetFileInfo(string path)
        {
            if(path == null)
                throw new ArgumentNullException("path");

            return GetFileInfo(new VirtualPath(path));
        }
        /// <summary>
        /// Deletes a file.
        /// </summary>
        /// <param name="path">Path to file in the file system.</param>
        /// <returns>True if file was delete; false if file was not found.</returns>
        public bool DeleteFile(VirtualPath path)
        {
            if(path == null)
                throw new ArgumentNullException("path");

            path = ResolvePath(path);

            return this.Drives[(DriveLetter)path.DriveLetter].DeleteFile(path);
        }
        /// <summary>
        /// Deletes a file.
        /// </summary>
        /// <param name="path">Path to file in the file system.</param>
        /// <returns>True if file was delete; false if file was not found.</returns>
        public bool DeleteFile(string path)
        {
            if(path == null)
                throw new ArgumentNullException("path");

            return DeleteFile(new VirtualPath(path));
        }
        /// <summary>
        /// Returns a value indicating whether a file exists.
        /// </summary>
        /// <param name="path">Path of file to check for.</param>
        /// <returns>Value indicating whether file exists.</returns>
        public bool FileExists(VirtualPath path)
        {
            if(path == null)
                throw new ArgumentNullException("path");

            path = ResolvePath(path);

            if(path.Elements.Count == 0)
                return false;

            var fileInfo = GetFileInfo(path);
            return fileInfo != null && (fileInfo.Attributes & VirtualFileAttributes.Directory) == 0;
        }
        /// <summary>
        /// Returns a value indicating whether a file exists.
        /// </summary>
        /// <param name="path">Path of file to check for.</param>
        /// <returns>Value indicating whether file exists.</returns>
        public bool FileExists(string path)
        {
            if(path == null)
                throw new ArgumentNullException("path");

            return FileExists(new VirtualPath(path));
        }
        /// <summary>
        /// Returns a value indicating whether a directory exists.
        /// </summary>
        /// <param name="path">Path of directory to check for.</param>
        /// <returns>Value indicating whether directory exists.</returns>
        public bool DirectoryExists(VirtualPath path)
        {
            if(path == null)
                throw new ArgumentNullException("path");

            path = ResolvePath(path);

            if(path.Elements.Count == 0)
                return true;

            var fileInfo = GetFileInfo(path);
            return fileInfo != null && (fileInfo.Attributes & VirtualFileAttributes.Directory) != 0;
        }
        /// <summary>
        /// Returns a value indicating whether a directory exists.
        /// </summary>
        /// <param name="path">Path of directory to check for.</param>
        /// <returns>Value indicating whether directory exists.</returns>
        public bool DirectoryExists(string path)
        {
            if(path == null)
                throw new ArgumentNullException("path");

            return FileExists(new VirtualPath(path));
        }
        /// <summary>
        /// Creates a new directory.
        /// </summary>
        /// <param name="path">Path of directory to create.</param>
        /// <returns>Value indicating whether directory exists.</returns>
        public bool CreateDirectory(VirtualPath path)
        {
            if(path == null)
                throw new ArgumentNullException("path");

            path = ResolvePath(path);

            if(path.Elements.Count == 0)
                return true;

            return this.Drives[(DriveLetter)path.DriveLetter].CreateDirectory(path);
        }
        /// <summary>
        /// Creates a new directory.
        /// </summary>
        /// <param name="path">Path of directory to create.</param>
        /// <returns>Value indicating whether directory exists.</returns>
        public bool CreateDirectory(string path)
        {
            return CreateDirectory(new VirtualPath(path));
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Raises the <see cref="E:CurrentDriveChanged"/> event.
        /// </summary>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void OnCurrentDriveChanged(EventArgs e)
        {
            var handler = this.CurrentDriveChanged;
            if(handler != null)
                handler(this, e);
        }
        #endregion

        #region Private Constants
        /// <summary>
        /// The file attributes which are used in GetDirectory filtering.
        /// </summary>
        private const VirtualFileAttributes FindAttributeMask = VirtualFileAttributes.Hidden | VirtualFileAttributes.System | VirtualFileAttributes.VolumeLabel | VirtualFileAttributes.Directory;
        #endregion
    }
}
